﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;
using System.Data;
using System.Data.Linq.Mapping;
using System.Data.SqlClient;
using System.Data.Common;
using System.Xml.Serialization;
using Littlefish.Data;
using Littlefish.Validation;

namespace Littlefish.Model
{
    
    /// <summary>
    /// Base Business Object class that wrappers LINQ as a data access layer.
    /// 
    /// Assume:
    /// Tables MUST HAVE a version (timestamp) field to allow simplified
    /// change tracking.
    /// 
    /// Each business object maps to a primary Entity/Table
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    /// <typeparam name="TContext"></typeparam>    
    public class BaseBusinessObject<TEntity, TContext>
        where TEntity : class, new()
        where TContext : BaseDataContext, new()
    {
        #region - Object Initialization -
        /// <summary>
        /// Base constructor - initializes the business object's
        /// context and table mapping info
        /// </summary>
        public BaseBusinessObject()
        {
            this.IntializeInternal();
            this.Initialize();
        }

        /// <summary>
        /// Constructore that allows passing in an existing DataContext
        /// so several business objects can share Context scope.
        /// </summary>
        /// <param name="context"></param>
        public BaseBusinessObject(TContext context)
        {
            this.IntializeInternal();
            this.Context = context;
            this.Initialize();
        }

        /// <summary>
        /// Internal method called to initialize various sub objects
        /// and default settings.
        /// </summary>
        private void IntializeInternal()
        {
            // *** Create the options for this business object
            this.Options = new BusinessObjectOptions();
        }

        /// <summary>
        /// Initializes the business object explicitly.
        /// 
        /// This method can be overridden by any subclasses that want to customize
        /// the instantiation behavior and should call back to the base method
        /// 
        /// The core features this method performs are:
        /// - Create a new context        
        /// </summary>
        protected virtual void Initialize()
        {
            // *** Create a default context
            if (this.Context == null)
            {
                if (!string.IsNullOrEmpty(this.Options.ConnectionString))
                    this.CreateContext(this.Options.ConnectionString);
                else
                    this.Context = this.CreateContext();
            }

            // *** Initialize Table Info 
            this.TableInfo = new TableInfo(Context, typeof(TEntity));
        }

        /// <summary>
        /// Creates an instance of the context object.
        /// </summary>
        /// <returns></returns>
        protected virtual TContext CreateContext()
        {
            return Activator.CreateInstance<TContext>() as TContext;
        }

        /// <summary>
        /// Allows creating a new context with a specific connection string.
        /// 
        /// The connection string can either be a full connection string or
        /// a connection string .Config entry.
        /// </summary>
        /// <param name="ConnectionString"></param>
        /// <returns></returns>
        protected virtual TContext CreateContext(string ConnectionString)
        {
            return Activator.CreateInstance(typeof(TContext), ConnectionString) as TContext;
        }
        #endregion

        #region - Properties -
        /// <summary>
        /// The provider factory used for this base implementation.
        /// </summary>
        public static DbProviderFactory ProviderFactory = DbProviderFactories.GetFactory("System.Data.SqlClient");

        /// <summary>
        /// Instance of the Data Context that is used for this class.
        /// Note that this is a primary instance only - other instances
        /// can be used in other situations.
        /// </summary>
        internal TContext Context { get; set; }

        /// <summary>
        /// Contains information about the primary table that is mapped
        /// to this business object. Contains table name, Pk and version
        /// field info. 
        /// 
        /// Values are automatically set by the constructor so ensure
        /// that the base constructor is always called.
        /// </summary>        
        protected TableInfo TableInfo { get; set; }

        private BusinessObjectOptions m_options;
        /// <summary>
        /// Contains options for the business object's operation
        /// </summary>
        public BusinessObjectOptions Options 
        {
            get { return m_options; }
            set
            {
                m_options = value;
                if (m_options != null)
                    this.ErrorHandler.ThrowExceptions = m_options.ThrowExceptions;
            }
        }

        /// <summary>
        /// Object that handles conversion of queries into concreate
        /// types and data structures. Allows conversion of queries
        /// to a data reader, data table as well as the standard 
        /// behaviors  ToList(), ToArray() using the entity class
        /// as its input.
        /// 
        /// This routine is useful in that it provides object parameters
        /// the abillity to return output in a variety of ways which
        /// makes anonymous type result more usable outside of local scope.
        /// </summary>
        public QueryConverter<TEntity, TContext> Converter
        {
            get
            {
                if (_QueryConverter == null)
                    _QueryConverter = new QueryConverter<TEntity, TContext>(this, this.Context as BaseDataContext);
                return _QueryConverter;
            }
        }
        private QueryConverter<TEntity, TContext> _QueryConverter = null;

        /// <summary>
        /// Instance of a locally managed entity object. Set with Load and New
        /// methods.
        /// </summary>
        public TEntity Entity { get; set; }
        
        [NonSerialized]
        ValidationErrorCollection m_validationErrors;
        /// <summary>
        /// A collection that can be used to hold errors. This collection
        /// is set by the AddValidationError method.
        /// </summary>
        [XmlIgnore]
        public ValidationErrorCollection ValidationErrors
        {
            get
            {
                if (this.m_validationErrors == null)
                    this.m_validationErrors = new ValidationErrorCollection();
                return m_validationErrors;
            }
        }

        private bool m_autoValidate = true;
        /// <summary>
        /// Determines whether or not the Save operation causes automatic
        /// validation
        /// </summary>        
        internal bool AutoValidate
        {
            get { return m_autoValidate; }
            set { m_autoValidate = value; }
        }
        #endregion

        #region - CRUD Methods -
        /// <summary>
        /// Loads an individual instance of an object
        /// </summary>
        /// <param name="pk"></param>
        /// <returns></returns>
        public virtual TEntity Load(object pk)
        {
            string sql = "select * from " + this.TableInfo.Tablename + " where " + this.TableInfo.PkField + "={0}";
            return this.LoadBase(sql, pk);
        }

        /// <summary>
        /// Loads a single record based on a generic SQL command. Can be used
        /// for customized Load behaviors where entities are loaded up.
        /// </summary>
        /// <param name="sqlLoadCommand"></param>
        /// <returns></returns>
        protected virtual TEntity LoadBase(string sqlLoadCommand, params object[] args)
        {
            this.ErrorHandler.SetError();

            try
            {
                TContext context = this.Context;

                // *** If disconnected we'll create a new context
                if (this.Options.TrackingMode == TrackingModes.Disconnected)
                    context = this.CreateContext();

                IEnumerable<TEntity> entityList = context.ExecuteQuery<TEntity>(sqlLoadCommand, args);

                TEntity entity = null;
                entity = entityList.Single();

                // *** Assign to local entity
                this.Entity = entity;

                // *** and return instance
                return entity;
            }
            catch (InvalidOperationException)
            {
                // *** Handles errors where an invalid Id was passed, but SQL is valid
                this.ErrorHandler.SetError("Couldn't load entity - invalid key provided.");
                this.Entity = this.NewEntity();
                return null;
            }
            catch (Exception ex)
            {
                // *** handles Sql errors
                this.ErrorHandler.SetError(ex);
                this.Entity = this.NewEntity();
            }

            return null;
        }

        /// <summary>
        /// Create a disconnected entity object
        /// </summary>
        /// <returns></returns>
        public virtual TEntity NewEntity()
        {
            this.ErrorHandler.SetError();

            try
            {
                TEntity entity = Activator.CreateInstance<TEntity>();

                //entity = Activator.CreateInstance(typeof(TEntity), "Parm1");

                this.Entity = entity;

                if (this.Options.TrackingMode == TrackingModes.Disconnected)
                    return entity;

                Table<TEntity> table = this.Context.GetTable(typeof(TEntity)) as Table<TEntity>;
                table.InsertOnSubmit(entity);

                return entity;
            }
            catch (Exception ex)
            {
                this.ErrorHandler.SetError(ex);
                return null;
            }
        }

        /// <summary>
        /// Saves a disconnected entity object
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public virtual bool Save(TEntity entity)
        {
            if (this.AutoValidate && !this.Validate())
                return false;

            // *** In connected mode any Save operation causes
            // *** all changes to be submitted.
            if (this.Options.TrackingMode == TrackingModes.Connected && entity == null)
            {
                try
                {
                    return this.SubmitChanges();
                }
                catch (Exception ex)
                {
                    this.ErrorHandler.SetError(ex);
                }
                return false;
            }

            if (entity == null)
                entity = this.Entity;

            using (TContext context = this.CreateContext())
            {
                try
                {
                    // *** Generically get the table
                    Table<TEntity> table = context.GetTable(typeof(TEntity)) as Table<TEntity>;

                    // *** BIG assumption here: version field and using Reflection - yuk!
                    //     can't see another way to figure out whether we're dealing with 
                    //     a new entity or not.
                    object tstamp = entity.GetType().GetProperty(this.TableInfo.VersionField).GetValue(entity, null);

                    // *** If there's no timestamp on the entity it's a new record
                    if (tstamp == null)
                        table.InsertOnSubmit(entity);
                    else
                        table.Attach(entity, true);

                    context.SubmitChanges();
                }
                catch (Exception ex)
                {
                    this.ErrorHandler.SetError(ex);
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Saves the internally stored Entity object
        /// by submitting all changes. Note this is the
        /// 'connected' version that submits all pending
        /// changes on the current data context.
        /// 
        /// For subclassing you should override the alternate
        /// entity signature.
        /// </summary>
        /// <returns></returns>
        public virtual bool Save()
        {
            return this.Save(null);
        }

        /// <summary>
        /// Cancel Changes on the current connected context
        /// </summary>
        public virtual void AbortChanges()
        {
            // *** Create a new context instance from scratch
            this.Context = this.CreateContext();
        }

        /// <summary>
        /// Saves changes on the current connected context.
        /// Preferrably you should use Save() rather than
        /// this method, but this provides a more low level
        /// direct context saving approach if you are
        /// working with connected data.
        /// 
        /// This method is also called from the Save() method.
        /// </summary>
        /// <param name="ConflictResolutionMode">Determines how change conflicts are applied</param>
        public bool SubmitChanges(ConflictResolutionModes ConflictResolutionMode)
        {
            try
            {
                // *** Always continue so we can get all the change conflicts
                this.Context.SubmitChanges(ConflictMode.ContinueOnConflict);
            }
            catch (ChangeConflictException ex)
            {
                switch (ConflictResolutionMode)
                {
                    // *** Pass the error out of here as is
                    // *** Let the client deal with it
                    case ConflictResolutionModes.None:
                        this.ErrorHandler.SetError(ex);
                        return false;

                    // *** Last one wins
                    case ConflictResolutionModes.ForceChanges:
                        this.Context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
                        break;

                    // *** All changes are aborted and entities update from database
                    case ConflictResolutionModes.AbortChanges:
                        this.Context.ChangeConflicts.ResolveAll(RefreshMode.OverwriteCurrentValues);
                        break;
                    //case ConflictResolutionModes.WriteNonConflictChanges
                    //    // TODO: Check this ConflictResoltuionmode behavior
                    //    context2.ChangeConflicts.ResolveAll(RefreshMode.KeepCurrentValues);
                    //    break;
                }
                try
                {
                    this.Context.SubmitChanges(ConflictMode.ContinueOnConflict);
                }
                catch (Exception ex2)
                {
                    this.ErrorHandler.SetError(ex2);
                    return false;
                }
            }
            catch (Exception ex)
            {
                this.ErrorHandler.SetError(ex);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Saves changes on the current connected context.
        /// Preferrably you should use Save() rather than
        /// this method, but this provides a more low level
        /// direct context saving approach if you are
        /// working with connected data.
        /// </summary>
        public bool SubmitChanges()
        {
            return this.SubmitChanges(this.Options.ConflictResolutionMode);
        }

        /// <summary>
        /// Deletes an entity specific by its Pk
        /// </summary>
        /// <param name="Pk"></param>
        /// <returns></returns>
        public bool Delete(object Pk)
        {
            int result = -1;
            try
            {
                result = this.Context.ExecuteNonQuery(
                                    "delete from " + this.TableInfo.Tablename + " where " + this.TableInfo.PkField + "=@Pk",
                                    this.Context.CreateParameter("@Pk", Pk));
            }
            catch (Exception ex)
            {
                this.ErrorHandler.SetError(ex);
                return false;
            }

            if (result < 1)
            {
                this.ErrorHandler.SetError("Nothing to delete");
                return false;
            }

            return true;
        }

        /// <summary>
        /// Deletes the active Entity object
        /// </summary>
        /// <returns></returns>
        public bool Delete()
        {
            return this.Delete(null);
        }

        /// <summary>
        /// Deletes a specific entity object.
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public bool Delete(TEntity entity)
        {
            if (entity == null)
            {
                entity = this.Entity;
                if (entity == null)
                    return false;
            }

            if (this.Options.TrackingMode == TrackingModes.Connected)
            {
                try
                {
                    Table<TEntity> table = this.Context.GetTable(typeof(TEntity)) as Table<TEntity>;
                    table.DeleteOnSubmit(entity);
                    return this.SubmitChanges();
                }
                catch (Exception ex)
                {
                    this.ErrorHandler.SetError(ex);
                }
                return false;
            }

            using (TContext context = this.CreateContext())
            {
                try
                {
                    Table<TEntity> table = context.GetTable(typeof(TEntity)) as Table<TEntity>;
                    table.DeleteOnSubmit(entity);
                    context.SubmitChanges();
                }
                catch (Exception ex)
                {
                    this.ErrorHandler.SetError(ex);
                    return false;
                }
            }

            return true;
        }
        #endregion

        #region - Validation -
        /// <summary>
        /// This method is used to validate business rules on the business object. 
        /// All errors are added to the <see cref="BaseBusinessObject.ValidationErrors"/> collection.
        /// 
        /// If the <see cref="BaseBusinessObject.AutoValidate"/> flag is set to true, <see cref="BaseBusinessObject.Save()"/>
        /// will automatically call this method. Inheriters should override <see cref="BaseBusinessObject.CheckValidationRules(TEntity)"/>
        /// to implement custom validation logic.
        /// <seealso>Class BaseBusinessObject Class ValidationErrorCollection</seealso>
        /// </summary>
        /// <returns>True if the entity is valid.</returns>
        public bool Validate(TEntity entity)
        {
            this.ValidationErrors.Clear();

            this.CheckValidationRules(entity);

            if (this.ValidationErrors.Count > 0)
            {
                this.ErrorHandler.SetError(this.ValidationErrors.ToString());
                return false;
            }

            return true;
        }

        /// <summary>
        /// Validates the current entity object
        /// </summary>
        /// <returns></returns>
        public virtual bool Validate()
        {
            return this.Validate(this.Entity);
        }

        protected virtual void CheckValidationRules(TEntity entity)
        {
        }
        #endregion

        #region - Error Handling -
        private ErrorHandler m_errorHandler;
        /// <summary>
        /// Handles errors for the business object.
        /// </summary>
        public ErrorHandler ErrorHandler
        {
            get
            {
                if (m_errorHandler == null)
                    m_errorHandler = new ErrorHandler(this.Options.ThrowExceptions);

                return m_errorHandler;
            }
            set
            {
                m_errorHandler = value;
                if (m_errorHandler != null)
                    m_errorHandler.ThrowExceptions = this.Options.ThrowExceptions;
            }
        }
        #endregion

        #region - Public Methods -
        public IQueryable GetEntities()
        {
            return from e in Context.GetTable<TEntity>()
                   select e;
        }
        #endregion
    }
}
